#include "stdafx.h"
#include "textPrinter.h"
#include "layoutDesc.h"
#include "layoutParser.h"
#include "utils.h"

#include "generateCPP.h"
#include "generateCSharp.h"

enum class ELanguage
{
	CPP,
	CSHARP,
};

bool EnumerateScriptFiles( const std::string& baseDir, std::vector< FileInfo >& outScriptFiles )
{
	// scan files
	ScanDir( baseDir, "", "*.proto", outScriptFiles );
	if ( outScriptFiles.empty() )
	{
		fprintf( stderr, "Folder '%s' does not contain any protocol files\n", baseDir.c_str() );
		return false;
	}

	fprintf( stdout, "Found %zu protocol scripts\n", outScriptFiles.size() );
	for( const FileInfo& file : outScriptFiles )
	{
		fprintf(stdout, "script: [ %s ]\n", file.m_relativePath.c_str() );
	}

	return true;
}

void GetOutputFilePaths( const std::string& targetDir, ELanguage lang, std::string& outPublicFile, std::string& outPrivateFile )
{
	if ( lang == ELanguage::CPP )
	{
		// .h is public, .cpp is private
		outPublicFile = targetDir + "protocol.h";
		outPrivateFile = targetDir + "protocol.cpp";
	}
	else if ( lang == ELanguage::CSHARP )
	{
		// .cs is public, _private.cs is private :)
		outPublicFile = targetDir + "protocol.cs";
		outPrivateFile = targetDir + "protocol_private.cs";
	}
}

bool VerifyCodeUpToDate( const std::string& targetDir, const std::vector< FileInfo >& scriptFiles, ELanguage lang )
{
	// check the data of the OLDEST output file
	FileTime oldestOutputFile = std::numeric_limits< FileTime >::max();
	bool hasMissingFiles = false;
	{
		// get the output files for given language
		std::string outputPublicFile, outputPrivateFile;
		GetOutputFilePaths( targetDir, lang, outputPublicFile, outputPrivateFile );

		// get the oldest output file, make sure that if the output file does not exist it will trigger the recompilation
		FileTime fileTime;
		if ( GetFileModificationTime( outputPublicFile, fileTime ) )
		{
			if ( fileTime < oldestOutputFile )
				oldestOutputFile = fileTime;
		}
		else
		{
			hasMissingFiles = true;
		}

		if ( GetFileModificationTime( outputPrivateFile, fileTime ) )
		{
			if ( fileTime < oldestOutputFile )
				oldestOutputFile = fileTime;
		}
		else
		{
			hasMissingFiles = true;
		}

	}

	// check the data of the NEWEST script file
	FileTime newestInputFile = 0;
	for ( const auto& info : scriptFiles )
	{
		FileTime fileTime;
		if ( GetFileModificationTime( info.m_absolutePath, fileTime ) ) // file exists
		{
			if ( fileTime > newestInputFile )
				newestInputFile = fileTime;
		}
	}

	// files are missing
	if ( hasMissingFiles )
	{
		fprintf( stdout, "There are missing output files. Rebuild needed.\n" );
		return false;
	}

	// if the input is newer than the output we need to rebuild
	if ( newestInputFile > oldestOutputFile )
	{
		fprintf( stdout, "Protocol files has changed. Rebuild needed.\n" );
		return false;
	}

#ifdef WIN32
	// debug: if there's a debugger ALWAYS rebuild
	if ( IsDebuggerPresent() )
		return false;
#endif

	// data seems to be up to date
	return true;
}

bool LoadScripts( const std::vector< FileInfo >& files, CLayoutDefinition& outDefinitions )
{
	bool noErrors = true;

	// process files
	for ( const FileInfo& file : files )
	{
		/// load source code
		const std::string code = LoadStringFromFile( file.m_absolutePath );
		if ( code.empty() )
		{
			fprintf( stderr, "%s(%d): Warning: File is empty\n", file.m_relativePath.c_str(), 1 );
			continue;
		}

#if _DEBUG
		fprintf( stdout, "[%s] size: %d\n", file.m_relativePath.c_str(), code.size() );
#endif

		/// parse layout definition
		CLayoutParser layoutParser;
		if ( !layoutParser.ParseLayout( outDefinitions, file.m_relativePath, code ) )
			noErrors = false;
	}

	// validate the definitions
	if ( !outDefinitions.Validate() )
	{
		return false;
	}

	// return true only if we had no errors
	return noErrors;
}

bool ProcessFiles( const std::string& absoluteInputPath, const std::string& absoluteOutputPathBase, const std::string& namespaceTxt, const ELanguage language )
{
	// scan for files
	std::vector< FileInfo > scriptFiles;
	EnumerateScriptFiles( absoluteInputPath, scriptFiles );

	// check if the output files are are up to date with respect to input files, if they are we don't have to process anything
	if ( VerifyCodeUpToDate( absoluteOutputPathBase, scriptFiles, language ) )
	{
		fprintf( stdout, "Protocol files are up to date.\n" );
		return true;
	}

	// load script definitions from the input files
	CLayoutDefinition defs;
	if ( !LoadScripts( scriptFiles, defs ) )
		return false; // error handled internally

	// no data
	if ( defs.GetGroups().empty() )
	{
		fprintf( stderr, "No protocol messages defined" );
		return false;
	}

	// get the output paths
	std::string outputPublicFile, outputPrivateFile; 
	GetOutputFilePaths( absoluteOutputPathBase, language, outputPublicFile, outputPrivateFile );

	// generate code
	if ( language == ELanguage::CPP )
	{
		/// generate header code
		{
			CTextPrinter printer;
			if ( !GenerateParserCode_CPP_Public( defs, printer, namespaceTxt ) )
				return false;

			/// store file
			bool wasUpToDate = false;
			printer.SaveToFile( outputPublicFile, wasUpToDate );
		}

		/// generate cpp code (actual parser)
		{
			CTextPrinter printer;
			if ( !GenerateParserCode_CPP_Private( defs, printer, namespaceTxt, outputPublicFile ) )
				return false;

			/// store file
			bool wasUpToDate = false;
			printer.SaveToFile( outputPrivateFile, wasUpToDate );
		}
	}
	else if ( language == ELanguage::CSHARP )
	{
		/// generate C# code
		{
			CTextPrinter printer;
			if ( !GenerateParserCode_CSharp_Public( defs, printer, namespaceTxt ) )
				return false;

			/// store file
			bool wasUpToDate = false;
			printer.SaveToFile( outputPublicFile, wasUpToDate );
		}

		/// generate C# code
		{
			CTextPrinter printer;
			if ( !GenerateParserCode_CSharp_Private( defs, printer, namespaceTxt ) )
				return false;

			/// store file
			bool wasUpToDate = false;
			printer.SaveToFile( outputPrivateFile, wasUpToDate );
		}
	}

	/// code generated
	return true;
}

int main( int argc, const char* argv[] )
{
	// no arguments
	if ( argc < 4 )
	{
		fprintf( stdout, "Universal Protocol Buffers code generate v1.0\n" );
		fprintf( stdout, "Written in 2015 by Tomasz \"RexDex\" Jonarski\n" );
		fprintf( stdout, "\n" );
		fprintf( stdout, "Usage:\n" );
		fprintf( stdout, "  proto cpp|cs <inputdir> <outputdir> [namespace]\n" );
		fprintf( stdout, "\n" );
		fprintf( stdout, "Generated files (C++):\n" );
		fprintf( stdout, "  protocol.h - definitions (semi public)\n" );
		fprintf( stdout, "  protocol.cpp - actual parsing code (strictly private)\n" );
		fprintf( stdout, "\n" );
		fprintf( stdout, "Generated files (C#):\n" );
		fprintf( stdout, "  protocol.cs - definitions (semi public)\n" );
		fprintf( stdout, "  protocol_private.cs - actual parsing code (strictly private)\n" );
		fprintf( stdout, "\n" );
		fprintf( stdout, "Default namespace: comm\n" );
		return -1;
	}

	// input file
	const std::string langName = argv[1];
	const std::string inputPath = argv[2];
	const std::string outputPath = argv[3];

	// choose language
	ELanguage lang;

	if ( langName == "cpp" )
	{
		lang = ELanguage::CPP;
	}
	else if ( langName == "cs" || langName == "csharp" )
	{
		lang = ELanguage::CSHARP;
	}
	else
	{
		fprintf( stderr, "Invalid language name '%s'\n", langName.c_str() );
		return -5;
	}

	// namespace
	std::string namespaceTxt = "comm";
	if ( argc >= 5 )
	{
		namespaceTxt = argv[4];
	}

	// parse definition file
	if ( !ProcessFiles( inputPath, outputPath, namespaceTxt, lang ) )
		return -2;

	// no errors
	return 0;
}

